10.x A Thread Pool Library in C++

第一课 Thread Pool Introducing


在之前我们引入线程的时候,我们比较了线程相比进程的优势。线程更小更轻,调度起来CPU的开销更小。此外,我们还介绍了协程,协程并不是一个调度单位。相比线程,协程的切换开销更小,而且协程的切换不需要进行用户态到内核态的切换。但协程并不能像线程那样可被 CPU 所调度。比较下来,线程的优势还是很明显的,我们想要一种方法让使用线程的开销更小些。

1.1 Pooling

1.1.1 Backgrounds

线程使用的开销主要在线程的创建、销毁和切换上。我们常使用从1加到1000来举例使用多线程的好处,如本来一个进程从1加到1000需要多长时间,我们可以将任务划分给10个线程,第一个线程从1加到100、第二个线程从101加到200....依此类推。

即使上面列举的例子十分简单的说明了多线程的好处,但不妨碍它是一个理想化的模型。因为线程的创建和销毁需要时间,而创建一个线程所用的时间可能已经超过单个线程从1加到1000所用的时间了。这就为我们留下了一些思考的问题:如何避免线程频繁创建和销毁所带来的开销?

1.1.2 Creating Threads Before Using Them

为了避免频繁实时的创建和销毁线程所带来的开销,我们可以在使用线程之前就预先创建一些线程,例如在服务器启动时创建一定数量的线程。这实际上就是池化技术的思想,即通过提前创建好资源并在运行过程中复用这些资源来提高系统的响应速度和鲁棒性。

1.1.3 How Many Threads do We Actually Need?

线程池中的线程并非越高越好,因为线程的切换(上下文切换)同样需要开销。当任务的粒度被划分得过分细时,线程切换所带来的系统开销占比就会非常大。甚至会大过多线程在多核系统上并行执行所带来的收益。所以我们需要平衡计算资源利用率和系统开销。

我们提到了线程的上下文切换开销。除此之外,线程还会带来一定的内存占用。在Linux中,一个线程默认8MB的栈空间(可以通过ulimit -s进行调整)。所以创建过多的线程可能会导致虚拟内存占用过多。

对于CPU密集型任务,常见的线程数配置策略基础公式如下:其中 表示处理器的物理核心数, 表示CPU的利用率(通常为0.7-0.9), 表示等待时间(wait)和计算时间(compute)的比率。

对于CPU密集型,一般线程池中的线程数大约在 ,而IO密集型线程池中的线程数量为

1.2 Thread Pool Patterns

线程池有以下几种不同的模式,每种模式都适用于不同的应用场景。最常见的要数fixed pool和cached pool,这两种池模式也适用于其他的object pools。

1.2.1 Fixed Thread Pool

固定线程池指线程池中的线程数量固定。这种线程池适用于任务数量相对稳定的场景。如果线程池中的所有线程都在忙时,新任务就需要在任务队列中等待。

1.2.2 Cached Thread Pool

缓存线程池允许线程池中的线程数量动态调整。适用于任务数量波动较大的场景。这种线程池会设置一个初始线程数量和线程阈值,池中的线程数就会在这个范围内波动。如果线程空闲超过一定时间,线程就会被终止并销毁。

1.2.3 Single Thread Pool

1.2.4 Scheduled Thread Pool

第二课 To See What Concurrency Library Really Is


第三课